/* Copyright (c) 2009 SpringShowcase, Inc.
 * All right reserved.
 * http://code.google.com/p/springshowcase/
 * This software is the confidential and proprietary information of BeyondWeb
 * , Inc. You shall not disclose such Confidential Information and
 * shall use it only in accordance with the terms of the license agreement
 * you entered into with BeyondWeb.
 *
 * Revision History
 * Author              Date             Description
 * ------------------   --------------    ------------------
 *  taeki.kim           2010. 4. 4.        
 */
package org.springshowcase.core.rpc;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.apache.commons.beanutils.MethodUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
import org.springshowcase.core.commons.utils.ControllerUtils;
import org.springshowcase.core.mvc.InitBinderSupport;
import org.springshowcase.core.mvc.JsonView;

// TODO: Auto-generated Javadoc
/**
 * The Class SpringDwrController.
 */
public class SpringRPCController extends MultiActionController {
	// ---------------------------------------------------------------------
	// Class Variable Declaration
	// ---------------------------------------------------------------------
	/** The Constant logger. */
	private static final Logger logger = LoggerFactory
			.getLogger(SpringRPCController.class);

	/** The Constant INVOKE_PARAM_NAME. */
	private static final String INVOKE_PARAM_NAME = "invoke";

	ThreadLocal<RPCInvokeBean> localThread = new ThreadLocal<RPCInvokeBean>();

	/** The init binder support. */
	@Autowired(required = false)
	private InitBinderSupport initBinderSupport;

	/** The validator. */
	@Autowired
	private Validator validator;

	private class RPCInvokeBean {
		/** The bind class. */
		private Class<?> bindClass;

		/** The return class. */
		private Class<?> returnClass;

		/** The validate. */
		private boolean validate = false;

		/** The result. */
		private BindingResult result;

		public Class<?> getBindClass() {
			return bindClass;
		}

		public void setBindClass(Class<?> bindClass) {
			this.bindClass = bindClass;
		}

		public Class<?> getReturnClass() {
			return returnClass;
		}

		public void setReturnClass(Class<?> returnClass) {
			this.returnClass = returnClass;
		}

		public boolean isValidate() {
			return validate;
		}

		public void setValidate(boolean validate) {
			this.validate = validate;
		}

		public BindingResult getResult() {
			return result;
		}

		public void setResult(BindingResult result) {
			this.result = result;
		}
	}

	// ---------------------------------------------------------------------
	// Constructor Method Declaration
	// ---------------------------------------------------------------------
	// ---------------------------------------------------------------------
	// DI Method (setter,getter) Declaration
	// ---------------------------------------------------------------------
	// ---------------------------------------------------------------------
	// this Method (public/private) Declaration
	// ---------------------------------------------------------------------
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.web.servlet.mvc.AbstractController#handleRequest(
	 * javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse)
	 */
	@Override
	@RequestMapping(method = RequestMethod.POST)
	public ModelAndView handleRequest(HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		logger.info("handle..SpringDwrController.handleRequest");
		ModelAndView mav = new ModelAndView();

		try {
			// setting thread local
			logger.debug("..setting local thread");
			localThread.set(new RPCInvokeBean());
			// parse invoke information
			Map<String, String> invokeMap = parseInvokeInfo(request
					.getParameter(INVOKE_PARAM_NAME));
			String beanId = invokeMap.get("beanId");
			String method = invokeMap.get("method");

			// invalid client parameter
			if (StringUtils.isEmpty(beanId) || StringUtils.isEmpty(method)) {
				throw new InvokeArgumentException();
			}

			// check invoke bean
			Object invokeBean = ControllerUtils.getBean(beanId);
			logger.debug("invoke bean name : " + invokeBean.toString());
			// not found Spring bean
			if (invokeBean == null) {
				logger.debug("Not Found Bean : " + beanId);
				throw new NotFoundInvokeBeanException("Not Found Bean");
			}

			// execute invoke method
			logger.debug("..execute local thread");
			
			Object returnObject = invokeMethod(invokeBean,
					method, request);
			if(returnObject ==null) {
				returnObject = new Object();
			}
			mav.addObject(JsonView.CONTENT_OBJECT,returnObject );

			RPCInvokeBean rpcBean = localThread.get();
			//
			if (rpcBean.isValidate()) {
				if(rpcBean.getResult()!=null&&rpcBean.getResult().hasErrors()) {
					mav.addObject(rpcBean.getResult());
				}
			}
		}
		finally {
			localThread.remove();
		}
		return mav;
	}

	/**
	 * <p>
	 * description about class
	 * </p>
	 * Invoke method.
	 * 
	 * @param beanId the bean id
	 * @param method the method
	 * @param request the request
	 * @return the object
	 * @throws Exception the exception
	 */
	private Object invokeMethod(Object invokeBean, String method,
			HttpServletRequest request) throws Exception {

		Class<?> invokeClass = invokeBean.getClass();
		// build bind & return class
		buldInvokeMethodInfo(getClassName(invokeBean), method);
		RPCInvokeBean rpcBean = localThread.get();
		boolean isMethodBind = false;
		// 바인딩 데이터가 있을 경우
		if (rpcBean.getBindClass() != null) {
			isMethodBind = true;
		}
		Object binderVO = new Object();
		// 바인딩 메서드가 있을 경우
		if (isMethodBind) {
			binderVO = rpcBean.getBindClass().newInstance();
			bind(request, binderVO);
		}
		Object returnObject = new Object();
		// invalid일 경우 중지하고 리턴 한다.
		if (rpcBean.getResult() != null && rpcBean.getResult().hasErrors()) {
			return returnObject;
		}

		// 바인딩 데이터가 있을 경우
		if (isMethodBind) {
			returnObject = MethodUtils.invokeMethod(invokeBean, method,
					binderVO);
		}
		else {
			returnObject = MethodUtils.invokeMethod(invokeBean, method, null);
		}
		return returnObject;
	}

	/**
	 * Gets the invoke method info.
	 * 
	 * @param className the class name
	 * @param method the method
	 * @return the invoke method info
	 * @throws Exception the exception
	 */
	private void buldInvokeMethodInfo(String className, String method)
			throws Exception {
		logger.debug("..get bind class and return class ");
		int bindMethodCnt = 0;
		Class invokeClass = Class.forName(className);
		Method[] methods = invokeClass.getMethods();
		Class<?>[] parameterTypes;

		RPCInvokeBean rpcBean = localThread.get();
		for (Method m : methods) {
			// // invoke 메서드일 경우만 실행
			if (StringUtils.equals(m.getName(), method)) {
				parameterTypes = m.getParameterTypes();
				// method override Exception
				if (bindMethodCnt > 0) {
					throw new MethodDuplicationException();
				}
				// 파라미터가 없을 경우
				if (parameterTypes.length == 0) {
					rpcBean.setReturnClass(m.getReturnType());
				}
				else if (parameterTypes.length == 1) {
					// exist @Valid Annotation
					if (m.getAnnotation(Valid.class) != null) {
						rpcBean.setValidate(true);
					}
					rpcBean.setBindClass(parameterTypes[0]);
					rpcBean.setReturnClass(m.getReturnType());
				}
				else {
					throw new TooManyParameterException();
				}
				bindMethodCnt++;
			}
		}
	}

	/**
	 * Gets the class name.
	 * 
	 * @param obj the obj
	 * @return the class name
	 */
	private String getClassName(Object obj) {

		String className = null;
		if (obj == null) {
			return "";
		}
		className = obj.toString();
		className = StringUtils.trimToEmpty(className.substring(0, className
				.indexOf("@")));
		return className;
	}

	/**
	 * <p>
	 * description about class
	 * </p>
	 * Parses the invoke info.
	 * 
	 * @param value the value
	 * @return the map
	 */
	private Map parseInvokeInfo(String value) {
		Map<String, String> map = new HashMap<String, String>();
		String invokeParam = StringUtils.trimToEmpty(value);
		String beanId = invokeParam.substring(0, invokeParam.indexOf("."));
		String method = invokeParam.substring(invokeParam.indexOf(".") + 1);

		map.put("beanId", StringUtils.trimToEmpty(beanId));
		map.put("method", StringUtils.trimToEmpty(method));
		return map;
	}

	// ---------------------------------------------------------------------
	// interface implement Method (public/private) Declaration
	// ---------------------------------------------------------------------

	// ---------------------------------------------------------------------
	// abstract implement Method (public/private) Declaration
	// ---------------------------------------------------------------------
	// ---------------------------------------------------------------------
	// abstract Method (public/private) Declaration
	// --------------------------------------------------u------------------
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.web.servlet.mvc.multiaction.MultiActionController
	 * #initBinder(javax.servlet.http.HttpServletRequest,
	 * org.springframework.web.bind.ServletRequestDataBinder)
	 */
	@InitBinder
	@Override
	protected void initBinder(HttpServletRequest request,
			ServletRequestDataBinder binder) throws Exception {
		logger.info("..invoke initBinder");
		if (this.initBinderSupport != null) {
			this.initBinderSupport.initBinder(binder);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.web.servlet.mvc.multiaction.MultiActionController
	 * #bind(javax.servlet.http.HttpServletRequest, java.lang.Object)
	 */
	protected void bind(HttpServletRequest request, Object command)
			throws Exception {
		logger
				.debug("Binding request parameters onto MultiActionController command");
		RPCInvokeBean rpcBean = localThread.get();
		ServletRequestDataBinder binder;
		binder = createBinder(request, command);
		binder.bind(request);
		// validation
		if (this.validator != null && rpcBean.isValidate()) {
			validator.validate(command, binder.getBindingResult());
		}
		// validation object
		BindingResult result = binder.getBindingResult();
		if (result.hasErrors()) {
			rpcBean.setResult(result);
		}
	}
}
